Map 是 ES6 新增的集合类型,大多数特性都可以通过 Object 类型实现,但是两者还有一些细微差异
使用 new 关键字和 Map 构造函数创建空的实例
const m = new Map();
如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象
// 使用嵌套数组初始化映射
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
alert(m1.size); // 3
// 使用自定义迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function*() {
yield ["key1", "val1"];
yield ["key2", "val2"];
yield ["key3", "val3"];
}
});
alert(m2.size); // 3
初始化之后,可以使用 set() 方法再添加键/值对;可以使用 get() 和 has() 进行查询;可以通过 size 属性获取映射中的键/值对的数量;可以使用 delete() 和 clear() 删除值。
const m = new Map();
alert(m.has("firstName")); // false
alert(m.get("firstName")); // undefined
alert(m.size); // 0
// set()方法返回映射实例,因此可以把多个操作连缀起来
m.set("firstName", "Matt").set("lastName", "Frisbie");
alert(m.has("firstName")); // true
alert(m.get("firstName")); // Matt
alert(m.size); // 2
m.delete("firstName"); // 只删除这一个键/值对
alert(m.has("firstName")); // false
alert(m.has("lastName")); // true
alert(m.size); // 1
m.clear(); // 清除这个映射实例中的所有键/值对
alert(m.has("firstName")); // false
alert(m.has("lastName")); // false
alert(m.size); // 0
与 Object 只能使用数值、字符串或符号作为键不同,Map 可以使用任何 JavaScript 数据类型作为键。与 Object 类似,映射的值是没有限制的。
const m = new Map();
const functionKey = function() {};
const symbolKey = Symbol();
const objectKey = new Object();
m.set(functionKey, "functionValue");
m.set(symbolKey, "symbolValue");
m.set(objectKey, "objectValue");
alert(m.get(functionKey)); // functionValue
alert(m.get(symbolKey)); // symbolValue
alert(m.get(objectKey)); // objectValue
// 即使看起来相同的key,实际也是不相等的
alert(m.get(function() {})); // undefined
与 Object 类型的一个主要差异是,Map 实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
alert(m.entries === m[Symbol.iterator]); // true
for (let pair of m.entries()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
开发中具体使用 Object 还是 Map 因人而异,考虑到需要兼容老版本浏览器,可能还是使用 Object 会更合适一些,但是随着技术的发展,可能会逐步的用 Map 进行替换,在此希望读者了解,至少能够读懂高手们的代码和思路
前面之所以介绍 Map 类型,有一部分原因也是为了引出 WeakMap 类型。WeakMap 是 Map 的“兄弟”类型,其 API 也是 Map 的子集。
使用 new 关键字实例化一个空的 WeakMap:
const wm = new WeakMap();
WeakMap 中的键只能是 Object 或者继承自 Object 的类型,其他类型会抛出 TypeError,但是值的类型没有限制。
const key1 = {id: 1},
key2 = {id: 2},
key3 = {id: 3};
const wm1 = new WeakMap([
[key1, "val1"],
[key2, "val2"],
[key3, "val3"]
]);
alert(wm1.get(key1)); // val1
alert(wm1.get(key2)); // val2
alert(wm1.get(key3)); // val3
// 只要有一个键无效就会抛出错误,导致整个初始化失败
const wm2 = new WeakMap([
[key1, "val1"],
["BADKEY", "val2"],
[key3, "val3"]
]);
// TypeError: Invalid value used as WeakMap key
typeof wm2;
// ReferenceError: wm2 is not defined
// 实在需要用原始值做键,可以先包装成对象再用
const stringKey = new String("key1");
const wm3 = new WeakMap([
stringKey, "val1"
]);
alert(wm3.get(stringKey)); // "val1"
初始化之后可以使用 set() 再添加键/值对,可以使用 get() 和 has() 查询,还可以使用 delete() 删除,用法与 Map 一致。
WeakMap 中“weak”表示弱映射的键是“弱弱地拿着”。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
const wm = new WeakMap();
wm.set({}, "val"); // 定义键/值对
以上例子中,因为没有指向 wm 的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收,这个键/值对就从弱映射中消失了,使其成为一个空映射。因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标
const wm = new WeakMap();
const container = {
key: {}
};
wm.set(container.key, "val");
function removeReference() {
container.key = null;
}
这一次,container 对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过,如果调用了 removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。
因为 WeakMap 中的键/值对任何时候都可能被销毁,所以其键/值对是不可迭代的
对于 WeakMap 的应用
维护私有变量:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId() {
return this.getPrivate(this.idProperty);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
}
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// 并不是真正私有的,外部依然可以访问到idProperty属性
alert(wm.get(user)[user.idProperty]); // 456
为了避免外部访问私有变量,可以使用闭包将 WeakMap 包装一下
const User = (() => {
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId() {
return this.getPrivate(this.idProperty);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
}
})();
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// 此时无法访问wm了,也就取不到user的idProperty属性了
alert(wm.get(user)[user.idProperty]);
// Uncaught ReferenceError: wm is not defined
保存DOM节点元数据
const m = new Map();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
m.set(loginButton, {disabled: true});
如果页面经过 JS 修改,登录按钮从 DOM 树中删掉了,但是由于映射中还保存着按钮的引用,所以对应的 DOM 节点仍然会逗留在内存中。如果改成下方代码,可以解决这个问题:
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(loginButton, {disabled: true});
当登录按钮节点从 DOM 树中被删除后,垃圾回收程序就可以立即释放其内存
Set 也是 ES6 新增的集合类型,在很多方面像是加强的 Map
使用 new 关键字和 Set 构造函数创建一个空集合:
const m = new Set();
如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象
// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
alert(s1.size); // 3
// 使用自定义迭代器初始化集合
const s2 = new Set({
[Symbol.iterator]: function*() {
yield "val1";
yield "val2";
yield "val3";
}
});
alert(s2.size); // 3
初始化完成后,可以使用 add() 增加值,使用 has() 查询,通过 size 取得元素数量,以及使用 delete() 和 clear() 删除元素:
const s = new Set();
alert(s.has("Matt")); // false
alert(s.size); // 0
s.add("Matt")
.add("Frisbie");
alert(s.has("Matt")); // true
alert(s.size); // 2
s.delete("Matt");
alert(s.has("Matt")); // false
alert(s.has("Frisbie")); // true
alert(s.size); // 1
s.clear(); // 销毁集合实例中的所有值
alert(s.has("Matt")); // false
alert(s.has("Frisbie")); // false
alert(s.size); // 0
与 Map 类似,Set 可以包含任何 JS 数据类型作为值
const s = new Set();
const functionVal = function() {};
const symbolVal = Symbol();
const objectVal = new Object();
s.add(functionVal);
s.add(symbolVal);
s.add(objectVal);
alert(s.has(functionVal)); // true
alert(s.has(symbolVal)); // true
alert(s.has(objectVal)); // true
// SameValueZero 检查意味着独立的实例不会冲突
alert(s.has(function() {})); // false
用作值的对象和其他“集合”类型在自己的内容或属性被修改时也不会改变
const s = new Set();
const objVal = {}, arrVal = [];
s.add(objVal);
s.add(arrVal);
objVal.bar = "bar";
arrVal.push("bar");
alert(s.has(objVal)); // true
alert(s.has(arrVal)); // true
delete() 返回一个布尔值,表示集合中是否存在要删除的值:
const s = new Set();
s.add('foo');
alert(s.size); // 1
s.add('foo');
alert(s.size); // 1
// 集合里有这个值
alert(s.delete('foo')); // true
// 集合里没有这个值
alert(s.delete('foo')); // false
Set 会维护值插入时的顺序,因此支持按顺序迭代,迭代方式与 Map 类似
const s = new Set(["val1", "val2", "val3"]);
alert(s.values === s[Symbol.iterator]); // true
alert(s.keys === s[Symbol.iterator]); // true
for (let value of s.values()) {
alert(value);
}
// val1
// val2
// val3
for (let value of s[Symbol.iterator]()) {
alert(value);
}
// val1
// val2
// val3
for (let pair of s.entries()) {
console.log(pair);
}
// ["val1", "val1"]
// ["val2", "val2"]
// ["val3", "val3"]
// 使用回调方式进行迭代
s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`));
// val1 -> val1
// val2 -> val2
// val3 -> val3
WeakSet 是 Set 的“兄弟”类型,其 API 也是 Set 的子集。
使用 new 关键字实例化一个空的 WeakSet:
const ws = new WeakSet();
弱集合中的值只能是 Object 或者继承自 Object 的类型,尝试使用非对象设置值会抛出 TypeError
构造函数可以接收一个可迭代对象,其中需要包含有效的值
const val1 = {id: 1},
val2 = {id: 2},
val3 = {id: 3};
// 使用数组初始化弱集合
const ws1 = new WeakSet([val1, val2, val3]);
alert(ws1.has(val1)); // true
alert(ws1.has(val2)); // true
alert(ws1.has(val3)); // true
// 只要有一个值无效就会抛出错误,导致整个初始化失败
const ws2 = new WeakSet([val1, "BADVAL", val3]); // TypeError: Invalid value used in WeakSet typeof ws2;
// 原始值可以先包装成对象再用作值
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]); alert(ws3.has(stringVal)); // true
初始化之后可以使用 add() 再添加新值,可以使用 has() 查询,还可以使用 delete() 删除:
const ws = new WeakSet();
const val1 = {id: 1}, val2 = {id: 2};
alert(ws.has(val1)); // false
ws.add(val1).add(val2);
alert(ws.has(val1)); // true
alert(ws.has(val2)); // true
ws.delete(val1); // 只删除这一个值
alert(ws.has(val1)); // false
alert(ws.has(val2)); // true
WeakSet 对于“weak”的解释与 WeakMap 基本一致,不再赘述,感兴趣直接看书上原文